﻿#region Header
// ------------------------ Licence / Copyright ------------------------
// 
// ScrumTable for Scrum Meeting Support
// Copyright © HSR - Hochschule für Technik Rapperswil
// All Rights Reserved
// 
// Author:
//  Michael Gfeller Silvan Gehrig Patrick Boos
// 
// ---------------------------------------------------------------------
#endregion

#region Usings

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using ScrumTable.Common;
using ScrumTable.Common.Collections;
using ScrumTable.Common.Text;

#endregion

namespace ScrumTable.DL.Data.Schema
{
    /// <summary>
    /// Specifies the base class for all schema entries.
    /// </summary>
    public abstract class SchemaBase
    {
        #region Declarations
        //--------------------------------------------------------------------
        // Declarations
        //--------------------------------------------------------------------

        private const char MultiValueSeparator = ',';
        private const string KeyValueSeparator = "=>";

        private const string FullNameFormat = "{0}.{1}";

        private const string DisplayNameProperty = "DisplayName";
        private const string MappedNameProperty = "MappedName";
        private const string AllowedValueMappingProperty = "AllowedValueMapping";
        private const string AllowedValuesProperty = "AllowedValues";
        private const string DefaultAllowedValuesProperty = "DefaultAllowedValues";
        private const string DefaultValueProperty = "DefaultValue";
        private const string MaxLengthProperty = "MaxLength";
        private const string TypeProperty = "Type";
        private const string SuggestedValues = "SuggestedValues";

        /// <summary>
        /// Specifies a dictionary for schema registrations.
        /// </summary>
        private readonly IDictionary<string, SchemaBase> _schemataRegistration;
        private readonly ICollection<SchemaBase> _schemata;
        private readonly IDictionary<string, string> _properties = new Dictionary<string, string>(4);

        private readonly IDictionary<string, string> _allowedValueMapping = new Dictionary<string, string>(0);
        private object _schemaDefaultValue;
        private string _mappedName;
        private string _displayName;
        private string _name = string.Empty;
        private string _fullName = string.Empty;
        private int _maxLength = -1;
        private bool _isInitialized = false;
        private IEnumerable<object> _schemaAllowedValues = new object[0];
        private IEnumerable<object> _schemaDefaultAllowedValues = new object[0];
        private IEnumerable<object> _schemaSuggestedValues = new object[0];
        private Type _schemaType;

        #endregion

        #region Properties
        //--------------------------------------------------------------------
        // Properties
        //--------------------------------------------------------------------

        /// <summary>
        /// Gets the schemata which are defined as schema references.
        /// </summary>
        public IEnumerable<SchemaBase> SchemaReferences
        {
            get
            {
                return from schema in _schemata
                       where schema.IsSchemaReference
                       select schema;
            }
        }

        /// <summary>
        /// Gets the sub schema with the given name or mapped name.
        /// </summary>
        /// <param name="schemaName">Name of the sub schema to retrieve.</param>
        /// <param name="searchByMappedName">True to search by mapped name.</param>
        /// <returns>Returns the retrieved sub schema or a null reference, if it could not be found.</returns>
        public SchemaBase this[string schemaName, bool searchByMappedName]
        {
            get
            {
                SchemaBase retrievedSchema = null;

                if (searchByMappedName)
                {
                    retrievedSchema = (from schema in _schemata
                                       where schema.SchemaMappedName == schemaName
                                       select schema).FirstOrDefault();
                }

                if (retrievedSchema == null)
                    retrievedSchema = this[schemaName];
                return retrievedSchema;
            }
        }

        /// <summary>
        /// Gets the sub schema with the given name.
        /// </summary>
        /// <param name="schemaName">Name of the sub schema to retrieve.</param>
        /// <returns>Returns the retrieved sub schema or a null reference, if it could not be found.</returns>
        public SchemaBase this[string schemaName]
        {
            get
            {
                if (_schemata.Count == 0)
                    return null;
                return _schemataRegistration[schemaName];
            }
        }

        /// <summary>
        /// Gets the name of the current schema. This name is used between the data layer
        /// and the business layer.
        /// </summary>
        public string SchemaName
        {
            get { return _name; }
            private set { _name = value; }
        }

        /// <summary>
        /// Gets the full name of the current schema. This name is used between the data layer
        /// and the business layer in addition with the parent schema name separated with a dot '.'.
        /// </summary>
        public string SchemaFullName
        {
            get { return !string.IsNullOrEmpty(_fullName) ? _fullName : _name; }
            private set { _fullName = value; }
        }

        /// <summary>
        /// Gets the maximum number of characters associated to the current field definition.
        /// </summary>
        public int SchemaMaxLength
        {
            get { return _maxLength; }
        }

        /// <summary>
        /// This name contains a human readable version of the SchemaName. If no display name is
        /// specified, the value of the <see cref="SchemaName"/> property will be returned.
        /// </summary>
        public string SchemaDisplayName
        {
            get { return HasSchemaDisplayName ? _displayName : SchemaName; }
        }

        /// <summary>
        /// Returns true if the current Schema has a mapped name defined.
        /// </summary>
        public bool HasSchemaDisplayName
        {
            get { return !string.IsNullOrEmpty(_displayName); }
        }

        /// <summary>
        /// This name maps an internal <see cref="SchemaName"/> to a name field of an exerntal system.
        /// </summary>
        public virtual string SchemaMappedName
        {
            get { return HasSchemaMappedName ? _mappedName : SchemaName; }
        }

        /// <summary>
        /// Returns true if the current Schema has a mapped name defined.
        /// </summary>
        public bool HasSchemaMappedName
        {
            get { return !string.IsNullOrEmpty(_mappedName); }
        }

        /// <summary>
        /// Returns true if the type of the current Schema corresponds to the
        /// type of the representing value.
        /// </summary>
        public virtual bool HasSchemaValueType
        {
            get { return false; }
        }

        /// <summary>
        /// Gets the sub schema entries of the current schema.
        /// </summary>
        public virtual IEnumerable<SchemaBase> Schemata { get { return _schemata; } }

        /// <summary>
        /// Gets the data type of the element which is represented by the current
        /// schema.
        /// </summary>
        public virtual Type SchemaType
        {
            get
            {
                if (IsSchemaReference)
                {
                    return typeof(ValueChain<string>);
                }
                return _schemaType;
            }
        }

        /// <summary>
        /// Returns true if the current schema element references on another schema element.
        /// </summary>
        public bool IsSchemaReference
        {
            get; protected set;
        }

        /// <summary>
        /// Returns true if referenced schema element is read only.
        /// </summary>
        public bool IsSchemaElementReadOnly
        {
            get; protected set;
        }

        /// <summary>
        /// Gets a read only dictionary which contains the schema properties.
        /// </summary>
        public IDictionary<string, string> SchemaProperties
        {
            get { return _properties.ToReadOnly(); }
        }

        /// <summary>
        /// Gets the allowed values for the current named schema instance.
        /// </summary>
        public IEnumerable<object> SchemaAllowedValues
        {
            get { return _schemaAllowedValues; }
            protected set { _schemaAllowedValues = (value ?? new object[0]); }
        }

        /// <summary>
        /// Gets the allowed values for the current named schema instance.
        /// </summary>
        public IEnumerable<object> SchemaDefaultAllowedValues
        {
            get { return _schemaDefaultAllowedValues; }
            protected set { _schemaDefaultAllowedValues = (value ?? new object[0]); }
        }

        /// <summary>
        /// Gets a list of suggested values for the current named schema instance.
        /// </summary>
        public IEnumerable<object> SchemaSuggestedValues
        {
            get { return _schemaSuggestedValues; }
            protected set { _schemaSuggestedValues = (value ?? new object[0]); }
        }

        /// <summary>
        /// Gets the default value for the current named schema instance.
        /// </summary>
        public object SchemaDefaultValue
        {
            get { return _schemaDefaultValue; }
            protected set { _schemaDefaultValue = value; }
        }

        /// <summary>
        /// Returns true if the allowed values property of the current cache field
        /// definition is overloaded by the schema.
        /// </summary>
        public bool HasSchemaAllowedValues { get { return (SchemaAllowedValues != null && SchemaAllowedValues.Count() > 0); } }

        /// <summary>
        /// Gets a read only dictionary which can be used to map values from
        /// an unspecified to an allowed value.
        /// </summary>
        public IDictionary<string, string> SchemaAllowedValueMapping
        {
            get { return _allowedValueMapping; }
        }

        /// <summary>
        /// Returns false if the current schema entry is compiled into the
        /// application. Otherwise the schema entry is added to the Schema.xml
        /// file by the end user.
        /// </summary>
        public virtual bool IsCustomSchema
        {
            get { return false; }
        }

        #endregion

        #region Constructors / Destructor
        //--------------------------------------------------------------------
        // Constructors / Destructor
        //--------------------------------------------------------------------

        /// <summary>
        /// Creates a new SchemaBase instance. This constructor can only
        /// be called from derived classes.
        /// </summary>
        protected SchemaBase()
        {
            _name = GetType().Name;
            _schemataRegistration = new Dictionary<string, SchemaBase>();
            _schemata = new List<SchemaBase>();
            _schemaType = GetType();

            IsSchemaReference = false;
            IsSchemaElementReadOnly = false;
            IndexFields();
        }

        #endregion

        #region Methods
        //--------------------------------------------------------------------
        // Methods
        //--------------------------------------------------------------------

        /// <summary>
        /// Returns true if the schema value mapping is appropriate to the
        /// given target values. These target values may be correspond to
        /// back-end system values which will be stored directly in the
        /// associated field.
        /// </summary>
        /// <param name="targetValues">Specifies the target values to validate.</param>
        public bool IsValidSchemaValueMapping(IEnumerable<string> targetValues)
        {
            if (targetValues == null) { return false; }
            if (SchemaAllowedValueMapping == null) { return false; }

            // 1. The values of the SchemaAllowedValueMapping targets to a back-end value. They must
            //    correspond, otherwise the mapping is invalid.
            // 2. The keys of the SchemaAllowedValueMapping targets to the SchemaAllowedValues collection.
            //    They must correspond, otherwise the mapping is invalid.
            return (!SchemaAllowedValueMapping.Values.HasDistinctElements(targetValues)
                    && !SchemaAllowedValueMapping.Keys.HasDistinctElements(SchemaAllowedValues));
        }

        /// <summary>
        /// Returns the schema property with the given attribute name.
        /// </summary>
        /// <typeparam name="T">Specifies the expected attribute type.</typeparam>
        /// <param name="attributeName">Name of the attribute to retrieve.</param>
        /// <param name="defaultValue">Specifies the default value if the type conversation failed.</param>
        /// <returns>Returns the retrieved attribute value.</returns>
        public T TryGetProperty<T>(string attributeName, T defaultValue = default(T))
        {
            string property = TryGetProperty(attributeName);

            if (property != null)
            {
                T propertyValue;

                if (ObjectUtil.TryConvertFromString(property, out propertyValue))
                {
                    return propertyValue;
                }
            }
            return defaultValue;
        }

        /// <summary>
        /// Returns the schema property with the given attribute name.
        /// </summary>
        /// <param name="attributeName">Name of the attribute to retrieve.</param>
        /// <returns>Returns the retrieved attribute value.</returns>
        public string TryGetProperty(string attributeName)
        {
            if (_properties.ContainsKey(attributeName))
            {
                return _properties[attributeName];
            }
            return null;
        }

        /// <summary>
        /// Retruns true if the current schema contains a sub schema with
        /// the given schema name.
        /// </summary>
        /// <param name="schemaName">Schema name to check if it's contained by the current schema.</param>
        /// <returns>Retruns true if the current schema contains a sub schema with the given name, otherwise false.</returns>
        public bool ContainsSchema(string schemaName)
        {
            return _schemataRegistration.ContainsKey(schemaName);
        }

        /// <summary>
        /// Retruns true if the current schema contains a sub schema with
        /// the given schema name.
        /// </summary>
        /// <param name="schemaName">Schema name to check if it's contained by the current schema.</param>
        /// <param name="searchByMappedName">True to search by mapped name.</param>
        /// <returns>Retruns true if the current schema contains a sub schema with the given name, otherwise false.</returns>
        public bool ContainsSchema(string schemaName, bool searchByMappedName)
        {
            bool found = false;

            if (searchByMappedName)
            {
                found = !(from schema in _schemata
                         where schema.SchemaMappedName == schemaName
                         select schema).IsEmpty();
            }
            return (found || ContainsSchema(schemaName));
        }

        /// <summary>
        /// Returns a <see cref="System.String"/> that represents this instance.
        /// </summary>
        /// <returns>
        /// A <see cref="System.String"/> that represents this instance.
        /// </returns>
        public override string ToString()
        {
            return SchemaName;
        }

        /// <summary>
        /// Converts the schema implicity to a corresponding string.
        /// </summary>
        /// <param name="schema">Schema to convert.</param>
        /// <returns>Returns the converted string instance.</returns>
        public static implicit operator string(SchemaBase schema)
        {
            return (schema != null) ? schema.ToString() : string.Empty;
        }

        /// <summary>
        /// Creates a new SchemaBase instance. This instance is used if a Schema.xml
        /// file contains a Schema mapping which is not declared as a Schema field.
        /// </summary>
        /// <param name="serializedSchema">Specifies the deserialized Xml node to create the new instance from.</param>
        /// <returns>Returns the created instance.</returns>
        protected virtual SchemaBase CreateCustomSchema(Serialization.Schema serializedSchema)
        {
            return new GenericSchema();
        }

        /// <summary>
        /// Fills the current schema base instance with the schema registrations
        /// in the given schemata.
        /// </summary>
        /// <param name="serializedRootSchema">Specifies the schemata definition with the raw xml data.</param>
        protected void ImportSchemaData(Serialization.Schema serializedRootSchema)
        {
            PreCondition.AssertNotNull(serializedRootSchema, "serializedRootSchema");

            FillProperties(serializedRootSchema, this);

            // fire prop initialized event
            OnPropertiesInitialized();

            if (serializedRootSchema.Schemata != null)
            {
                FillChildren(serializedRootSchema.Schemata);
            }
        }

        private void FillChildren(IEnumerable<Serialization.Schema> serializedSchemata)
        {
            // initializes the set of schemata defined in the xml file
            foreach (Serialization.Schema serializedSchema in serializedSchemata)
            {
                if (string.IsNullOrEmpty(serializedSchema.Name))
                {
                    continue;
                }

                if (!_schemataRegistration.ContainsKey(serializedSchema.Name))
                {
                    AddNewSchema(serializedSchema);
                }
                FillSchema(serializedSchema, _schemataRegistration[serializedSchema.Name]);
            }

            // initializes the compiled schemata objects
            foreach (SchemaBase schema in Schemata)
            {
                if (!schema._isInitialized)
                {
                    FillSchema(null, schema);
                }
            }
        }

        private void FillProperties(Serialization.Schema serializedSchema, SchemaBase schema)
        {
            if (serializedSchema != null && serializedSchema.Properties != null)
            {
                foreach (Serialization.Property property in serializedSchema.Properties)
                {
                    schema._properties[property.Key] = property.Value;
                }
            }
        }

        private void FillSchema(Serialization.Schema serializedSchema, SchemaBase schema)
        {
            FillProperties(serializedSchema, schema);

            // init schema full name
            schema.SchemaFullName = string.Format(FullNameFormat, SchemaFullName, schema.SchemaName);

            // fire prop initialized event
            schema.OnPropertiesInitialized();
            schema.FillChildren(
                serializedSchema != null && serializedSchema.Schemata != null
                ? (IEnumerable<Serialization.Schema>)serializedSchema.Schemata
                : new Serialization.Schema[0]);
        }

        private void AddNewSchema(Serialization.Schema serializedSchema)
        {
            SchemaBase newSchema = CreateCustomSchema(serializedSchema);
            newSchema.SchemaName = serializedSchema.Name;

            _schemataRegistration[serializedSchema.Name] = newSchema;
            _schemata.Add(newSchema); // store order of schema elements
        }

        private void IndexFields()
        {
            FieldInfo[] fields = GetType().GetFields(BindingFlags.Instance | BindingFlags.Public);

            foreach (FieldInfo field in fields)
            {
                if (typeof(SchemaBase).IsAssignableFrom(fields[0].FieldType))
                {
                    SchemaBase fieldValue = (SchemaBase)field.GetValue(this);
                    fieldValue.SchemaName = field.Name;

                    _schemataRegistration[field.Name] = fieldValue;
                    _schemata.Add(fieldValue); // store order of schema elements
                }
            }
        }

        private void InitAllowedValueMapping()
        {
            if (_properties.ContainsKey(AllowedValueMappingProperty))
            {
                string allowedValuesProperty = TryGetProperty(AllowedValueMappingProperty);

                foreach (string mappingPair in allowedValuesProperty.Split(new[] { MultiValueSeparator }, StringSplitOptions.RemoveEmptyEntries))
                {
                    string[] keyValuePair = mappingPair.Split(
                        new[] { KeyValueSeparator },
                        StringSplitOptions.RemoveEmptyEntries);

                    if (keyValuePair.Length == 2)
                    {
                        string key = keyValuePair[0].Trim();
                        string value = keyValuePair[1].Trim();

                        _allowedValueMapping[key] = value;
                    }
                }
            }
        }

        private void InitMaxLength()
        {
            if (_properties.ContainsKey(MaxLengthProperty))
            {
                _maxLength = TryGetProperty<int>(MaxLengthProperty);
            }
        }

        private void InitMappedName()
        {
            if (_properties.ContainsKey(MappedNameProperty))
            {
                _mappedName = _properties[MappedNameProperty];
            }
        }

        private void InitDisplayName()
        {
            if (_properties.ContainsKey(DisplayNameProperty))
            {
                _displayName = _properties[DisplayNameProperty];
            }
        }

        private void InitAllowedValues()
        {
            _schemaAllowedValues = ParseMultiValuePropertyData(AllowedValuesProperty).Distinct().ToList();
        }

        private void InitDefaultAllowedValues()
        {
            _schemaDefaultAllowedValues = ParseMultiValuePropertyData(DefaultAllowedValuesProperty).Distinct().ToList();
        }

        private void InitDefaultValueProperty()
        {
            if (_properties.ContainsKey(DefaultValueProperty))
            {
                _schemaDefaultValue = _properties[DefaultValueProperty].ChangeType(SchemaType);
            }
        }

        private void InitSuggestedValues()
        {
            _schemaSuggestedValues = ParseMultiValuePropertyData(SuggestedValues).Distinct().ToList();
        }

        private void InitSchemaType()
        {
            if (_properties.ContainsKey(TypeProperty))
            {
                Type loadedType = Type.GetType(_properties[TypeProperty], false, true);

                if (loadedType != null)
                {
                    _schemaType = loadedType;
                }
            }
        }
        
        private IList<string> ParseMultiValuePropertyDataRaw(string propertyName)
        {
            if (_properties.ContainsKey(propertyName))
            {
                return StringUtil.SplitTrim(_properties[propertyName], MultiValueSeparator);
            }
            return new string[0];
        }

        private IList<object> ParseMultiValuePropertyData(string propertyName)
        {
            IList<string> rawAllowedValues = ParseMultiValuePropertyDataRaw(propertyName);
            object[] allowedValues = new object[rawAllowedValues.Count];
            rawAllowedValues.ForEachIndex((idx, val) => allowedValues[idx] = rawAllowedValues[idx].ChangeType(SchemaType));
            return allowedValues;
        }

        #endregion

        #region Events
        //--------------------------------------------------------------------
        // Events
        //--------------------------------------------------------------------

        /// <summary>
        /// This method will be called after the SchemaProperties have been
        /// initialized. 
        /// </summary>
        protected virtual void OnPropertiesInitialized()
        {
            InitDisplayName();
            InitMappedName();
            InitSchemaType();
            InitAllowedValueMapping();
            InitAllowedValues();
            InitDefaultAllowedValues();
            InitDefaultValueProperty();
            InitMaxLength();
            InitSuggestedValues();
            _isInitialized = true;
        }

        #endregion
    }
}
